Redux是一個獨立的架構,它不一定要使用在React上,因為它的機制可以補足React比較弱的地方,所以常常和React一起提到。Redux用來管理狀態的變化,而React主要關注在component呈現的地方。前幾天我們已經把Redux的action、reducer、store設定好,今天要介紹把Redux和React結合在一起的地方!
在binding Redux和React之前,還有一個觀念要說明,就是container component和presentational component,我們要先區分這兩類components,加上Redux會更得心應手。
我們需要使用container component來連結Redux,只有它會知道Redux的存在,它通常只是一個 容器 的概念,裡面裝其他components。它負責訂閱Redux(subscribe)並把Redux的state和actions傳給子元件,只傳入需要的資訊,像是取state tree需要用到的部分state傳入即可。Container component的子元件可以是container component或是presentational component。
大部份的component都會是presentational component,這類component不會知道Redux的存在,接收父層傳來的props,用props處理並顯示mockup和style,components通常也比較少有自己的state,如果有也是自己內部UI相關的state。
不免俗的,讓我用一個table比較一下:
| Presentational component | Container component
| ------------- | -------------
和Redux有接觸 | 否 | 是
關注的部分 | 如何顯示style、mockup | 如何處理、更新data
取得資料 | 從props取得 | 訂閱Redux state
改變資料 | 從props取得callback | 發送Redux action
產生方式 | 手寫產生 | 通常由react-redux產生
把Redux和React binding在一起,需要再透過另一個package react-redux,先使用npm安裝:
npm install react-redux --save
我們可以透過connnect()
來幫我們產生container component,它會使用store.subscribe()
來偵聽某些state tree,並且提供props傳給子元件。它會回傳一個函數,就是container component,並且可以接收四個參數,以下先介紹常用的前兩個。
(1) mapStateToProps
當有定義這個參數給connect()
,表示這個container component訂閱Redux store的更新。它是一個回傳部分state tree的function,這些state就是container component要傳給其他子元件的props,當這些state被更新,會自動subscribe並傳回更新後的state tree。
這邊以todos的範例來說明,會回傳需要訂閱的state並指定給:
const mapStateToProps = (state) => {
return {
todos: state.todos,
filter: state.filter
};
};
(2) mapDispatchToProps
它是一個回傳action creator的function,讓container component可以dispatch的actions都需要傳入,並且綁定dispatch,讓接收到props的component可以直接呼叫這些function。
這邊直接以範例說明:
const mapDispatchToProps = (dispatch) => {
return {
addTask: (task) => { dispatch(addTask(task)) },
editTask: (idx, task) => { dispatch(editTask(idx, task)) },
deleteTask: (idx) => { dispatch(deleteTask(idx)) },
toggleTask: (idx) => { dispatch(toggleTask(idx)) }
};
};
// 接收到props的component可以直接呼叫 this.props.addTask('some tasks');
沒有錯,每個action creator如果都要列出來,有時候會顯得有點麻煩&冗長,還記得Day19的時候我們有介紹到 bindActionCreators 這個function嗎?它是Redux提供的一個方法,可以把一個或多個action creator轉換成同樣名稱當作key的object,並且加上dispatch,在這邊我們可以使用它來直接綁定同一個檔案裡的action。
import { bindActionCreators } from 'redux';
import * as TodosActions from './actions/todos';
const mapDispatchToProps = (dispatch) => {
return {
todosActions: bindActionCreators(TodosActions, dispatch)
};
}
// 接收到props的component可以直接呼叫 this.props.todosActions.addTask('some tasks');
最後,我們使用connect()把這兩個參數傳入:
import { connect } from 'react-redux';
const TodoAppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);
export default TodoAppContainer;
如果當你的component沒有使用到這些參數,可以這樣設定:
// 不傳state,也不傳actions
export default connect()(App);
// 傳入state,但不傳actions
export default connect(mapStateToProps)(App);
// 不傳state,但傳入actions
export default connect(null, mapDispatchToProps)(App);
加入connect()
之後,就可以把原先我們設定的一些functions和todos變數都移除。
完整的app.js如下:
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import classNames from 'classnames';
import * as TodosActions from './actions/todos';
import TodoList from './components/TodoList';
import TodoAdd from './components/TodoAdd';
import '../css/style.css';
class App extends Component {
render() {
// 改由props接收actions
const { todos, todosActions } = this.props;
return (
<div>
<h1 className={classNames('title')}>React Todo List</h1>
<TodoAdd addTask={todosActions.addTask} />
<TodoList
todos={todos}
saveTask={todosActions.editTask}
deleteTask={todosActions.deleteTask}
completeTask={todosActions.toggleTask}
/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
todos: state.todos,
filter: state.filter
};
};
const mapDispatchToProps = (dispatch) => {
return {
todosActions: bindActionCreators(TodosActions, dispatch)
};
};
const TodoAppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);
export default TodoAppContainer;
接下來我們要把Redux store傳給container component,最直覺的方式就是上面再包一層root,把store當作props傳給我們的container component,所以react-redux提供了一個 <Provider> component,用來包住我們的container component。
完整的index.js如下:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import todoApp from './reducers';
import TodoAppContainer from './app';
let store = createStore(todoApp);
ReactDOM.render(
<Provider store={store}>
<TodoAppContainer />
</Provider>,
document.getElementById('main')
);
現在我們已經完成綁定Redux和React,原本React版本的todos,已經變成React Redux版本的囉!到目前這個步驟的檔案放在Git 上囉!接下來我想補足其他沒寫到的code。
這邊要加上先前有提到的filter功能,透過這個步驟,可以更清楚看到如何切分container component。
Step1. 新增一個actions/filter.js:
import * as types from '../constants/ActionTypes';
// action creator
export function setFilter(filter){
return {
type: types.SET_FILTER,
filter
};
}
並且加一個constants/ActionTypes.js:
export const SET_FILTER = 'SET_FILTER';
改變之前我們reducers/filter.js設定的常數
case types.SET_FILTER:
Step2. 新增一個components/Filter.js:
import React, { Component } from 'react';
class Filter extends Component {
render() {
const { filter, filterActions } = this.props;
return (
<div>
<button
onClick={() => filterActions.setFilter('SHOW_ALL')}
disabled={ filter === 'SHOW_ALL' }
>All</button>
<button
onClick={() => filterActions.setFilter('SHOW_COMPLETED')}
disabled={ filter === 'SHOW_COMPLETED' }
>Completed</button>
<button
onClick={() => filterActions.setFilter('SHOW_UNCOMPLETED')}
disabled={ filter === 'SHOW_UNCOMPLETED' }
>Uncompleted</button>
</div>
);
}
}
export default Filter;
Step3. 新增一個containers/FilterContainer.js
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as FilterActions from '../actions/filter';
import Filter from '../components/Filter';
class App extends Component {
render() {
return (
<div className="filter">
<Filter {...this.props} />
</div>
);
}
}
const mapStateToProps = (state) => {
return {
filter: state.filter
};
};
const mapDispatchToProps = (dispatch) => {
return {
filterActions: bindActionCreators(FilterActions, dispatch)
};
};
const FilterContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);
export default FilterContainer;
Step4. 加入FilterContainer到TodoAppContainer中
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import classNames from 'classnames';
import * as TodosActions from '../actions/todos';
import FilterContainer from '../containers/FilterContainer';
import TodoList from '../components/TodoList';
import TodoAdd from '../components/TodoAdd';
import '../../css/style.css';
class App extends Component {
render() {
const { filter, todos, todosActions } = this.props;
// 這個container component包含了另一個container component(FilterContainer)
return (
<div>
<h1 className={classNames('title')}>React Todo List</h1>
<TodoAdd addTask={todosActions.addTask} />
<FilterContainer />
<TodoList
todos={todos}
filter={filter}
saveTask={todosActions.editTask}
deleteTask={todosActions.deleteTask}
completeTask={todosActions.toggleTask}
/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
todos: state.todos,
filter: state.filter
};
};
const mapDispatchToProps = (dispatch) => {
return {
todosActions: bindActionCreators(TodosActions, dispatch)
};
};
const TodoAppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);
export default TodoAppContainer;
Step5. 顯示todos的時候,判斷目前filter狀態
components/TodoItem.js:
_renderItems() {
const { filter, todos, saveTask, deleteTask, completeTask } = this.props;
let list = [];
todos.forEach((todo, idx) => {
// 這邊為了保留原本的idx,判斷filter狀態來顯示
if (filter === 'SHOW_ALL' ||
(filter === 'SHOW_COMPLETED' && todo.isCompleted) ||
(filter === 'SHOW_UNCOMPLETED' && !todo.isCompleted)) {
list.push(
<TodoItem
key={idx}
idx={idx}
todo={todo}
saveTask={saveTask}
deleteTask={deleteTask}
completeTask={completeTask}
/>);
}
});
return list;
}
這樣就完成,加上filter的功能囉!這部分最主要是展示container component可以依照功能分開,而container component又可以包含container component或是presentational component。
今天真的是很長的一篇XD,但我們已經學會Redux囉!完整的程式碼已經放在Git 上。